﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;

namespace CloneUsingExpressionTree
{
    public static class CloneExtension
    {
        private static class CloneManager<T> where T : class, new()
        {
            private readonly static Action<T, T> copyPropertiesValues = null;
            private readonly static Action<T, T> copyFieldsValues = null;

            static CloneManager()
            {
                copyFieldsValues = GenerateCopyFieldsValuesAction();
                copyPropertiesValues = GenerateCopyPropertiesValuesAction();
            }

            private static Action<T, T> GenerateCopyPropertiesValuesAction()
            {
                var properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(p => p.SetMethod != null && p.SetMethod.IsPublic && p.GetMethod != null && p.GetMethod.IsPublic).ToArray();

                return GenerateCopyValuesAction(properties);
            }

            private static Action<T, T> GenerateCopyFieldsValuesAction()
            {
                var fields = typeof(T).GetFields(BindingFlags.Instance | BindingFlags.Public).Where(f => !f.IsInitOnly).ToArray();

                return GenerateCopyValuesAction(fields);
            }

            private static Action<T, T> GenerateCopyValuesAction(MemberInfo[] members)
            {
                if (members.Length == 0)
                    return (s, t) => { };

                var source = Expression.Parameter(typeof(T), "source");
                var target = Expression.Parameter(typeof(T), "target");

                var block = Expression.Block(
                    members.Select(m => Expression.Assign(
                        Expression.PropertyOrField(target, m.Name),
                        Expression.PropertyOrField(source, m.Name)
                    ))
                );

                return Expression.Lambda<Action<T, T>>(block, source, target).Compile();
            }

            public static T GetClone(T source, ClonePart part)
            {
                var newItem = new T();

                if ((part & ClonePart.Properties) == ClonePart.Properties)
                    copyPropertiesValues(source, newItem);
                if ((part & ClonePart.Fields) == ClonePart.Fields)
                    copyFieldsValues(source, newItem);

                return newItem;
            }
        }

        public static T Clone<T>(this T source, ClonePart parts) where T : class, new()
        {
            var cloneable = source as ICloneable;
            if (cloneable != null)
                return (T)cloneable.Clone();

            return CloneManager<T>.GetClone(source, parts);
        }

        public static T Clone<T>(this T source) where T : class, new()
        {
            return Clone(source, ClonePart.Fields | ClonePart.Properties);
        }
    }
}
